/* ------------------------------------------------------------
 *  Bunny project - simulator
 *
 * ------------------------------------------------------------ 
 */
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Collections;

class IVector
{
	IVector() { x = y = 0; cost = Integer.MAX_VALUE; }
	IVector(int _x, int _y) { x = _x; y = _y; cost = Integer.MAX_VALUE; }
	
	boolean equal(IVector b)
	{
		return x == b.x && y == b.y;
	}
	
	int distanceSquared(IVector b)
	{
		int dx = x-b.x;
		int dy = y-b.y;
		
		return dx*dx + dy*dy;
	}
	
	float distance(IVector b)
	{
		return sqrt( (float) distanceSquared(b) );
	}
	
	int x, y, cost;
}

class IVectorComparator implements Comparator<IVector>
{
	public int compare(IVector x, IVector y)
	{
		if (x.cost < y.cost)
		{
			return -1;
		}
		else if (x.cost > y.cost)
		{
			return 1;
		}
		else
		{
			return 0;
		}
	}
}

class Bunny
{
	private boolean visible;
	private PVector pos;
	private PVector target;
	
	private ArrayList<PVector> path;
	private int curPathIndex;
	private float timeToHide, timeToChange;
	PImage bunnyImage;
	
	private boolean toEat;
	private int eatFrom;
	
	public Bunny()
	{
		pos = null;
		path = null;
		target = null;
		visible = false;
		timeToHide = 0.0;
		toEat = false;
		eatFrom = -1;
		
		timeToChange = random(MIN_TIME_CHANGE, MAX_TIME_CHANGE);
		
		bunnyImage = loadImage(BUNNY_IMAGE);	
	}
	
	public void setToEat( int _from)
	{
		toEat = true; 
		eatFrom = _from;
	}
	
	public void render( float dT )
	{
		tick(dT);
		
		if (!visible)
		{
			// don't render if bunny is hidden
			return;
		}
		
		pushStyle();
		{
			/*
			fill(255, 0, 0);
			ellipse( pos.x, pos.y, 20, 20 );
			*/
			image( bunnyImage, pos.x, pos.y );
		}
		popStyle();		
	}
	
	void followPath( float dT)
	{
		float delta = dT * BUNNY_SPEED;
			
		// interpolate between pos and path[curIndex]
		PVector next = path.get(curPathIndex);
		PVector direction = new PVector(next.x - pos.x, next.y - pos.y);
		float dMag = direction.mag();
		
		if (dMag < delta)
		{
			// advance to the next position
			curPathIndex++;
			if (curPathIndex >= path.size() - 1)
			{
				// reached my destination
				path = null;
				
				if (!toEat)
				{
					timeToHide = BUNNY_HIDE_TIME;
				}
				else
				{
					timeToHide = BUNNY_EAT_TIME;
					sim.patches.get(eatFrom).startEat();
				}
			}
			else
			{
				next = path.get(curPathIndex);
				direction = new PVector(next.x - pos.x, next.y - pos.y);
			}
		}
		else
		{
			direction.normalize();
			direction.x *= delta; direction.y *= delta;
			
			// advance position
			pos.x += direction.x;
			pos.y += direction.y;
		}
	}
	
	void setPath(ArrayList<PVector> _newPath)
	{
		visible = true;
		path = _newPath;
		curPathIndex = min(0, _newPath.size()-1);
		timeToChange = random(MIN_TIME_CHANGE, MAX_TIME_CHANGE);
		
		if (toEat)
		{
			toEat = false;
			sim.patches.get(eatFrom).endEating();
		}		
	}

	void tick( float dT )
	{
		if (path != null)
		{
			followPath( dT );
		}
		else
		{
			timeToHide -= dT;
			if (timeToHide <= 0.0)
			{
				visible  = false;
			}
		}
		timeToChange -= dT;
	}	
}

class Simulator
{
	
	// private member variables	
	private ArrayList<FoodPatch> patches;
	private int availablePatches;
	private ArrayList<Bunny> bunnies;
	
	private PImage theMask;	
	private PImage theMap;
	private float maskScale;
	
	private boolean readTangibles;
	
	public void setReadTangibles()
	{
		readTangibles = true;
	}
	
	void registerPatches( HashMap<Integer, IVector> dishes )
	{
		// loop through dishes
		for (int i = 0; i < patches.size(); i++)
		{
			FoodPatch patch = patches.get(i);
			int id = patch.id;
			
			IVector location = dishes.get( id );
			if (location != null)
			{
				patch.refill( location.x, location.y );
				availablePatches--;
			}
			else
			{
				// remove the patch
				if (patch.visible)
				{
					patch.available = true;
					patch.visible = false;
					availablePatches++;
				}
				
			}
		}
		
		// remove the patches that are not there
	}
	
	public void render( float dT )
	{
		// render patches
		for (int i = 0; i < patches.size(); i++)
		{
			FoodPatch patch = patches.get(i);
			patch.draw();
		}

		// render all the bunnies
		for (int i = 0; i < bunnies.size(); i++)
		{
			// evaluate the bunny
			Bunny bunny = bunnies.get(i);
			bunny.render( dT );
			
			if (bunny.timeToChange <= 0.0) 
			{
				// search for food patch with food
				FoodPatch targetPatch = null;
				int patchIndex = -1;
				
				for (int j = 0; j < patches.size(); j++)
				{
					FoodPatch p = patches.get(j);
					if (p.isVisible() && p.hasAnyFood())
					{
						targetPatch = p;
						patchIndex = j;
					}
				}
				
				
				PVector newTarget = null;
				if (targetPatch == null)
				{
					// no food patch available, just walk
					// to a random place
					newTarget = getRandomPosition();
				}
				else
				{
					newTarget = environment.getGridCellCenter(
						new IVector(targetPatch.x, targetPatch.y)
					);
				}
				
				IVector start = worldToMask( bunny.pos );
				IVector finish = worldToMask( newTarget );
						
				// resolve A*
				IVector path[][] = aStar(start, finish);
				
				if (path == null)
				{
					newTarget = getRandomPosition();
					finish = worldToMask( newTarget);
					path = aStar(start, finish);
					targetPatch = null;
				}
				
				ArrayList<PVector> wPath = translatePath( path, finish );
				bunny.setPath( wPath );
				
				if (targetPatch != null) {
					// bunny will eat from here
					bunny.setToEat( patchIndex );
				}
			}
		}
		
	}
	
	int getAvailablePatchCount()
	{
		return availablePatches;
	}
	
	ArrayList<PVector> translatePath( IVector[][] path, IVector finish )
	{
		ArrayList<PVector> wPath = new ArrayList<PVector>();
		IVector cur = finish;
		
		while (cur != null)
		{
			wPath.add( maskToWorld( cur ) );
			cur = path[ cur.x ][ cur.y ];
		}
		
		// reverse
		Collections.reverse( wPath );
		
		return wPath;
	}
	
	PVector getRandomPosition()
	{
		boolean done = false;
		PVector pos = null;
		while (!done)
		{
			pos = new PVector(
				floor(random( 0, theMap.width )),
				floor(random( 0, theMap.height ))
			);	
			
			IVector maskPos = worldToMask( pos );
			if ( pixelValid( maskPos.x, maskPos.y ) )
			{
				done = true;
			}
		}
		return pos;
	}
	
	IVector worldToMask(PVector w)
	{
		return new IVector(
			floor( w.x * maskScale),
			floor( w.y * maskScale)
		);
	}
	
	PVector maskToWorld(IVector a)
	{
		return new PVector(
			(float) a.x / maskScale,
			(float) a.y / maskScale
		);
	}
	
	
	Simulator(PImage _theMap, PImage _theMask, int bunnyCount, int patchCount)
	{
		
		bunnies = new ArrayList<Bunny>();
		patches = new ArrayList<FoodPatch>();
		
		theMap = _theMap;
		theMask = _theMask;
		
		// calculate the scale different between the mask and the real map
		maskScale = (float) theMask.width / (float) theMap.width;
		
		// create bunnies
		for (int i = 0; i < bunnyCount; i++)
		{
			Bunny bunny = new Bunny();
			bunny.pos = getRandomPosition();
			bunnies.add( bunny );
		}
		
		// create patches
		for (int i = 0; i < patchCount; i++)
		{
			FoodPatch patch = new FoodPatch( 0, 0, FOOD_PATCH_COLOR[i], i );
			patches.add( patch );
			patch.setIdle();
		}
		availablePatches = patchCount;
		readTangibles = false;
	}
	
	public IVector[][] aStar( IVector start, IVector target )
	{
		Comparator<IVector> comparator = new IVectorComparator();
		PriorityQueue<IVector> openset = new PriorityQueue<IVector>(
			theMask.width * theMask.height, comparator
		);
		
		// temporary variables
		boolean closedset[][] = new boolean[theMask.width][theMask.height];
		boolean inqueue[][] = new boolean[theMask.width][theMask.height];
		int g_score[][] = new int[theMask.width][theMask.height];
		IVector[][] path = new IVector[theMask.width][theMask.height];
			
		// initialize
		for (int i = 0; i < theMask.width; i++)
		{
			for (int j = 0; j < theMask.height; j++)
			{
				closedset[i][j] = false;
				inqueue[i][j] = false;
				g_score[i][j] = Integer.MAX_VALUE;
				path[i][j] = null;
			}
		}
		
		g_score[ start.x ][ start.y ] = 0;
		start.cost = heuristic_cost( start, target );
		openset.add( start );
		
		while ( openset.size() != 0 )
		{
			IVector k = openset.poll();
			inqueue[ k.x ][ k.y ] = false;
			if ( k.equal(target) )
			{
				return path;
			}
			
			closedset[ k.x ][ k.y ] = true;
			ArrayList<IVector> neighbors = getValidNeighbors( k, closedset );
			
			// loop through valid neighbors
			for (int n = 0; n < neighbors.size(); n++)
			{
				IVector l = neighbors.get(n);
				int h = heuristic_cost(l, target);
				int tentative_g_score = g_score[k.x][k.y] + k.distanceSquared( l );
				boolean tentative_is_better = false;

							
				if ( !inqueue[ l.x ][ l.y ] )
				{
					l.cost = tentative_g_score + h;
					openset.add( l );
					inqueue[ l.x ][ l.y ] = true;
					tentative_is_better = true;
				}
				else if (tentative_g_score < g_score[ l.x ][ l.y ])
				{
					tentative_is_better = true;
				}
				
				if (tentative_is_better)
				{
					g_score[ l.x ][ l.y ] = tentative_g_score;
					path[ l.x ][ l.y ] = k;
				}
			}
			
		}
		
		return null;
	}
	
	
	
	private ArrayList<IVector> getValidNeighbors( IVector k, boolean closedset[][] )
	{
		ArrayList<IVector> neighbors = new ArrayList<IVector>();
		
		boolean w = k.x > 0;
		boolean e = k.x < theMask.width  - 1;
		boolean s = k.y < theMask.height - 1;
		boolean n = k.y > 0;
		
		int Wx = k.x - 1;
		int Ex = k.x + 1;
		int Ny = k.y - 1;
		int Sy = k.y + 1;
		
		if ( w && !closedset[ Wx ][ k.y ] && pixelValid( Wx, k.y ) )
			neighbors.add( new IVector(Wx, k.y) );
		if ( e && !closedset[Ex][k.y] && pixelValid(Ex,k.y) )
			neighbors.add( new IVector(Ex, k.y) );
		if ( s && !closedset[k.x][Sy] && pixelValid(k.x, Sy) )
			neighbors.add( new IVector(k.x, Sy) );
		if ( n && !closedset[k.x][Ny] && pixelValid(k.x, Ny) )
			neighbors.add( new IVector(k.x, Ny) );
		
		if ( n && w && !closedset[Wx][Ny] && pixelValid(Wx,Ny) )
			neighbors.add( new IVector(Wx,Ny) );
		
		if ( n && e && !closedset[Ex][Ny] && pixelValid(Ex,Ny) )
			neighbors.add( new IVector(Ex,Ny) );
		
		if ( w && s && !closedset[Wx][Sy] && pixelValid(Wx,Sy) )
			neighbors.add( new IVector(Wx, Sy) );
		
		if ( e && s && !closedset[Ex][Sy] && pixelValid(Ex, Sy) )
			neighbors.add( new IVector(Ex,Sy) );
		
		return neighbors;
	}
	
	private boolean pixelValid(int x, int y)
	{
		color c = theMask.pixels[ y*theMask.width + x ];
		if (c != color(0, 0, 0, 255))
		{
			return true;
		}
		else
		{
			return false;
		}	
	}

	
	private int heuristic_cost(IVector a, IVector b)
	{
		return a.distanceSquared( b );
	}

        public int getFoodDishAmount(int id) {
          return (int) ( patches.get(id).getLeftOver() / 10.0 );
        }
        
        public void setFoodDishAmount(int id, float amount) {
          patches.get(id).setRefillAmount( amount * 10 );
        }

}

 
